/**************************************************************************
* This file is part of Hawkengine.
*
* Hawkengine is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Hawkengine is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Hawkengine.  If not, see <http://www.gnu.org/licenses/>.
**************************************************************************/


/**************************************************************************
* Dependencies
**************************************************************************/
#include "engine/sound.h"
#include "engine/settings.h"
#include "engine/util.h"

/**************************************************************************
* Preprocessing
**************************************************************************/

/**************************************************************************
* Types
**************************************************************************/
struct sound_node;
typedef struct sound_node sound_node_type;
struct sound_node
{
  sound_type* sound;
  sound_node_type* next;
};

typedef struct
{
  const sound_type* sound;
  float delay;
} task_type;

struct task_node;
typedef struct task_node task_node_type;
struct task_node
{
  task_type task;
  task_node_type* next;
  task_node_type* prev;
};

/**************************************************************************
* Variables
**************************************************************************/
extern const file_data_type applause_wav;
extern const file_data_type boo_wav;
extern const file_data_type cheering_wav;
extern const file_data_type click_wav;
extern const file_data_type bubble_wav;
UCHAR sound_volume = 255;
sound_type sound_applause;
sound_type sound_boo;
sound_type sound_cheering;
sound_type sound_click;
sound_type sound_bubble;

sound_delete_func_type sound_delete_func = NULL;
sound_generate_func_type sound_generate_func = NULL;
sound_generate_func_type sound_generate_ambient_func = NULL;
sound_init_func_type sound_init_func = NULL;
sound_is_playing_func_type sound_is_playing_func = NULL;
sound_pause_func_type sound_pause_func = NULL;
sound_play_at_func_type sound_play_at_func = NULL;
sound_set_listener_func_type sound_set_listener_func = NULL;
sound_stop_func_type sound_stop_func = NULL;
sound_uninit_func_type sound_uninit_func = NULL;

/**************************************************************************
* Static Variables
**************************************************************************/
static sound_node_type* sounds = NULL;
static const int INITIAL_TASK_NODES = 16;
static task_node_type* tasks;
static task_node_type* unused_tasks;

/**************************************************************************
* Function Prototypes
**************************************************************************/
static void add_node(task_node_type** node_list, task_node_type* node);
static void add_sound(sound_type* sound);
static void create_unused_task_node(void);
static task_node_type* get_unused_task_node(void);
static void remove_node(task_node_type** node_list, task_node_type* node);

/**************************************************************************
* Function Implementations
**************************************************************************/
static void add_node(task_node_type** node_list, task_node_type* node)
{
  node->next = (*node_list);
  if (*node_list) {
    // other nodes exist, connect them back to new node
    node->prev = (*node_list)->prev;
    (*node_list)->prev = node;
  }

  // update destination node with given node
  (*node_list) = node;
}

static void add_sound(sound_type* sound)
{
  sound_node_type* new_node = malloc(sizeof(sound_node_type));
  new_node->sound = sound;
  new_node->next = sounds;
  sounds = new_node;
}

static void create_unused_task_node(void)
{
  task_node_type* node;
  node = malloc(sizeof(task_node_type));
  memset(node, 0, sizeof(task_node_type));
  add_node(&unused_tasks, node);
}

static task_node_type* get_unused_task_node(void)
{
  task_node_type* node;

  if (!unused_tasks) {
    // no more nodes, create one
    create_unused_task_node();
  }
  node = unused_tasks;
  remove_node(&unused_tasks, node);

  // initialize the node
  memset(node, 0, sizeof(task_node_type));

  return node;
}

static void remove_node(task_node_type** node_list, task_node_type* node)
{
  if ((*node_list) == node) {
    // head of the list, update the node_list pointer
    (*node_list) = node->next;
  }
  if (node->prev) {
    node->prev->next = node->next;
  }
  if (node->next) {
    node->next->prev = node->prev;
  }
  node->next = node->prev = NULL;
}

BOOL sound_generate(const file_data_type* file_data, sound_type* sound)
{
  if (sound_generate_func) {
    if (!sound_generate_func(file_data, sound)) {
      return FALSE;
    }
    add_sound(sound);
  }
  return TRUE;
}

BOOL sound_generate_ambient(const file_data_type* file_data, sound_type* sound)
{
  if (sound_generate_ambient_func) {
    if (!sound_generate_ambient_func(file_data, sound)) {
      return FALSE;
    }
    add_sound(sound);
  }
  return TRUE;
}

void sound_init(void)
{
  int i, temp;
  temp = sound_volume;
  settings_get(OPTIONS_SETTINGS, "volume", &temp);
  sound_volume = (UCHAR)temp;

  sound_prepare_environment();

  if (sound_init_func) {
    sound_init_func();
  }

  // setup engine sounds
  sound_generate_ambient(&applause_wav, &sound_applause);
  sound_generate_ambient(&boo_wav, &sound_boo);
  sound_generate_ambient(&cheering_wav, &sound_cheering);
  sound_generate_ambient(&click_wav, &sound_click);
  sound_generate_ambient(&bubble_wav, &sound_bubble);

  tasks = NULL;
  unused_tasks = NULL;

  // create some initial task nodes
  for (i = 0; i < INITIAL_TASK_NODES; i++) {
    create_unused_task_node();
  }
}

BOOL sound_is_playing(const sound_type* sound)
{
  if (sound_is_playing_func) {
    return sound_is_playing_func(sound);
  }
  return TRUE;
}

BOOL sound_pause(const sound_type* sound)
{
  if (sound_pause_func) {
    return sound_pause_func(sound);
  }
  return TRUE;
}

BOOL sound_play(const sound_type* sound, BOOL loop)
{
  static const float ORIGIN[3] = {0, 0, 0};
  return sound_play_at(sound, ORIGIN, loop);
}

BOOL sound_play_at(const sound_type* sound, const float location[3], BOOL loop)
{
  if (sound_play_at_func) {
    return sound_play_at_func(sound, location, loop);
  }
  return TRUE;
}

void sound_schedule(const sound_type* sound, float delay)
{
  task_node_type* task_node;

  // get task node
  task_node = get_unused_task_node();
  task_node->task.sound = sound;
  task_node->task.delay = delay;

  // now add node to head of list
  if (tasks) {
    tasks->prev = task_node;
  }
  task_node->next = tasks;
  tasks = task_node;
}

BOOL sound_set_listener(const camera_type* camera)
{
  if (sound_set_listener_func) {
    return sound_set_listener_func(camera);
  }
  return TRUE;
}

BOOL sound_stop(const sound_type* sound)
{
  if (sound_stop_func) {
    return sound_stop_func(sound);
  }
  return TRUE;
}

void sound_toggle(void)
{
  static UCHAR last_volume = 255;
  int temp;

  if (sound_volume > 0) {
    last_volume = sound_volume;
    sound_volume = 0;
  } else {
    sound_volume = last_volume;
  }

  temp = sound_volume;
  settings_set(OPTIONS_SETTINGS, "volume", INT_DATA, &temp);
  settings_save();
}

void sound_uninit(void)
{
  task_node_type* task_node;
  sound_node_type* sound_node;

  if (sound_delete_func) {
    while (sounds) {
      sound_delete_func(sounds->sound);
      sound_node = sounds;
      sounds = sounds->next;
      free(sound_node);
    }
  }

  if (sound_uninit_func) {
    sound_uninit_func();
  }

  sounds = NULL;

  while (tasks) {
    task_node = tasks;
    tasks = tasks->next;
    free(task_node);
  }
  tasks = NULL;

  while (unused_tasks) {
    task_node = unused_tasks;
    unused_tasks = unused_tasks->next;
    free(task_node);
  }
  unused_tasks = NULL;
}

void sound_update(float secs)
{
  task_node_type* task_node;
  task_node_type* curr_task_node;

  if (secs <= 0) {
    return;
  }

  task_node = tasks;
  while (task_node) {
    curr_task_node = task_node;
    task_node = task_node->next;

    // handle delays
    curr_task_node->task.delay -= secs;
    if (curr_task_node->task.delay > 0) {
      continue;
    } else {
      if (curr_task_node->task.sound) {
        sound_play(curr_task_node->task.sound, FALSE);
      }
      curr_task_node->task.delay = 0;
      // move the task node into the unused nodes
      remove_node(&tasks, curr_task_node);
      add_node(&unused_tasks, curr_task_node);
    }
  }
}

#if defined(SOUND_OPENAL)
#define AMBIENT_SOUND_DISTANCE 32

static ALCdevice* openal_device = NULL;
static ALCcontext* openal_context = NULL;

static BOOL openal_create_sound_handles(const file_data_type* file_data,
                                        UINT source_count, sound_type* sound);
static BOOL openal_generate(const file_data_type* file_data, sound_type* sound);
static BOOL openal_generate_ambient(const file_data_type* file_data, sound_type* sound);
static void openal_init(void);
static BOOL openal_is_playing(const sound_type* sound);
static BOOL openal_load_wav_file(const file_data_type* file_data, UINT buffer_handle);
static BOOL openal_pause(const sound_type* sound);
static BOOL openal_play_at(const sound_type* sound, const float location[3], BOOL loop);
static BOOL openal_set_listener(const camera_type* camera);
static BOOL openal_stop(const sound_type* sound);
static void openal_uninit(void);

static BOOL openal_create_sound_handles(const file_data_type* file_data,
                                        UINT source_count, sound_type* sound)
{
  ALenum error;
  UINT i;
  float source_pos[] = {0, 0, 0};
  float source_vel[] = {0, 0, 0};


  sound->source_count = _MIN(source_count, MAX_SOURCES);

  // generate openal objects
  alGenBuffers(1, &(sound->buffer_handle));
  if ((error = alGetError()) == AL_NO_ERROR) {
    alGenSources(sound->source_count, sound->source_handles);
    if (((error = alGetError()) == AL_NO_ERROR) &&
        (openal_load_wav_file(file_data, sound->buffer_handle))) {
      for (i = 0; i < sound->source_count; i++) {
        // attach buffer to source
        alSourcei(sound->source_handles[i], AL_BUFFER, sound->buffer_handle);
        alSourcef(sound->source_handles[i], AL_PITCH, 1.0);
        alSourcef(sound->source_handles[i], AL_GAIN, 1.0);
        alSourcefv(sound->source_handles[i], AL_POSITION, source_pos);
        alSourcefv(sound->source_handles[i], AL_VELOCITY, source_vel);
        alSourcei(sound->source_handles[i], AL_LOOPING, FALSE);
        alSourcefv(sound->source_handles[i], AL_POSITION, DEFAULT_CAMERA.pos);
      }
      if ((error = alGetError()) == AL_NO_ERROR) {
        return TRUE;
      }
    }
  }

  // something went wrong
  memset(sound, 0, sizeof(sound_type));
  return FALSE;
}

static void openal_delete(sound_type* sound)
{
  UINT i;
  for (i = 0; i < sound->source_count; i++) {
    if (alIsSource(sound->source_handles[i])) {
      alDeleteSources(1, &(sound->source_handles[i]));
    }
  }
  if (alIsBuffer(sound->buffer_handle)) {
    alDeleteBuffers(1, &(sound->buffer_handle));
  }
}

static BOOL openal_generate(const file_data_type* file_data, sound_type* sound)
{
  memset(sound, 0, sizeof(sound_type));

  if (!openal_create_sound_handles(file_data, 1, sound)) {
    return FALSE;
  }
  return TRUE;
}

static BOOL openal_generate_ambient(const file_data_type* file_data, sound_type* sound)
{
  float x, y;
  UINT i;

  memset(sound, 0, sizeof(sound_type));

  if (!openal_create_sound_handles(file_data, 4, sound)) {
    return FALSE;
  }

  for (i = 0; (i < sound->source_count) && (i < 4); i++) {
    x = y = AMBIENT_SOUND_DISTANCE;
    if (i < 2) {
      x *= -1;
    }
    if ((i >=1) && (i <= 2)) {
      y *= -1;
    }
    sound->source_offsets[i][X] = x;
    sound->source_offsets[i][Y] = y;
    sound->source_offsets[i][Z] = 0;
  }
  return TRUE;
}

static void openal_init(void)
{
  // get default device
  openal_device = alcOpenDevice(NULL);
  if (openal_device) {
    // create context
    openal_context = alcCreateContext(openal_device, 0);
    if (openal_context) {
      // activate context
      alcMakeContextCurrent(openal_context);

      sound_set_listener(&DEFAULT_CAMERA);
    } else {
      return;
    }
  } else {
    return;
  }
  return;
}

static BOOL openal_is_playing(const sound_type* sound)
{
  int state;
  UINT i;
  ALenum error;
  for (i = 0; i < sound->source_count; i++) {
    alGetSourcei(sound->source_handles[i], AL_SOURCE_STATE, &state);
    if ((error = alGetError()) != AL_NO_ERROR) {
      return FALSE;
    }
    if (state == AL_PLAYING) {
      return TRUE;
    }
  }
  return FALSE;
}

static BOOL openal_load_wav_file(const file_data_type* file_data, UINT buffer_handle)
{
  ALenum alenum;
  const UCHAR* curr_data;
  BOOL found_header;
  UINT chunk_length;
  UINT magic;
  USHORT audio_format;
  USHORT num_channels;
  UINT sample_frequency;
  UINT byte_rate;
  USHORT block_align;
  USHORT bits_per_sample;
  int size;

  curr_data = file_data->data;
  found_header = FALSE;

  curr_data += 8;
  memcpy(&magic, curr_data, sizeof(magic));
  curr_data += sizeof(magic);

  // check for wav identifier
  if (magic != 0x45564157) {
    return FALSE;
  }

  while (TRUE) {
    memcpy(&magic, curr_data, sizeof(magic));
    curr_data += sizeof(magic);
    memcpy(&chunk_length, curr_data, sizeof(chunk_length));
    curr_data += sizeof(chunk_length);

    if (!chunk_length || !magic) {
      return FALSE;
    }

    if (magic == 0x20746d66) {
      // format
      found_header = TRUE;

      if (chunk_length < 16) {
        return FALSE;
      }

      // read header data
      memcpy(&audio_format, curr_data, sizeof(audio_format));
      curr_data += sizeof(audio_format);
      memcpy(&num_channels, curr_data, sizeof(num_channels));
      curr_data += sizeof(num_channels);
      memcpy(&sample_frequency, curr_data, sizeof(sample_frequency));
      curr_data += sizeof(sample_frequency);
      memcpy(&byte_rate, curr_data, sizeof(byte_rate));
      curr_data += sizeof(byte_rate);
      memcpy(&block_align, curr_data, sizeof(block_align));
      curr_data += sizeof(block_align);
      memcpy(&bits_per_sample, curr_data, sizeof(bits_per_sample));
      curr_data += sizeof(bits_per_sample);

      if (!audio_format || !num_channels || !sample_frequency ||
          !byte_rate || !block_align || !bits_per_sample) {
        return FALSE;
      }

      curr_data += (chunk_length - 16);

      if (audio_format != 1) {
        // not PCM
        return FALSE;
      }
    } else if (magic == 0x61746164) {
      // data
      if (!found_header) {
        return FALSE;
      }
      // load up the sound
      size = (file_data->size - (curr_data - file_data->data));
      if (bits_per_sample == 8) {
        if (num_channels == 1) {
          alenum = AL_FORMAT_MONO8;
        } else if (num_channels == 2) {
          alenum = AL_FORMAT_STEREO8;
        } else {
          return FALSE;
        }
      } else if (bits_per_sample == 16) {
        if (num_channels == 1) {
          alenum = AL_FORMAT_MONO16;
        } else if (num_channels == 2) {
          alenum = AL_FORMAT_STEREO16;
        } else {
          return FALSE;
        }
      } else {
        return FALSE;
      }
      alBufferData(buffer_handle, alenum, curr_data, size, sample_frequency);
      if ((alenum = alGetError()) != AL_NO_ERROR) {
        return FALSE;
      }
      return TRUE;
    } else {
      curr_data += chunk_length;
    }

    if (chunk_length & 1) {
      curr_data += 1;
    }

    if (curr_data >= (file_data->data + file_data->size)) {
      // don't go past end of buffer
      return FALSE;
    }
  }

  return FALSE;
}

static BOOL openal_pause(const sound_type* sound)
{
  ALenum error;
  UINT i;

  for (i = 0; i < sound->source_count; i++) {
    if (sound->source_handles[i] <= 0) {
      return FALSE;
    }

    alSourcePause(sound->source_handles[i]);
    if ((error = alGetError()) != AL_NO_ERROR) {
      return FALSE;
    }
  }
  return TRUE;
}

static BOOL openal_play_at(const sound_type* sound, const float location[3], BOOL loop)
{
  ALenum error;
  UINT i;
  int j;
  float sound_loc[3];

  if (sound_is_playing(sound)) {
    sound_stop(sound);
  }

  // don't play if there is no volume
  if (sound_volume <= 0) {
    return TRUE;
  }

  for (i = 0; i < sound->source_count; i++) {
    if (sound->source_handles[i] <= 0) {
      return FALSE;
    }
    for (j = 0; j < 3; j++) {
      sound_loc[j] = (location[j] + sound->source_offsets[i][j]);
    }
    alSourcefv(sound->source_handles[i], AL_POSITION, sound_loc);
    alSourcef(sound->source_handles[i], AL_GAIN, (float)sound_volume / 255.0);
    alSourcei(sound->source_handles[i], AL_LOOPING, loop);
    alSourcePlay(sound->source_handles[i]);
    if ((error = alGetError()) != AL_NO_ERROR) {
      return FALSE;
    }
  }
  return TRUE;
}

static BOOL openal_set_listener(const camera_type* camera)
{
  ALenum error;
  float velocity[] = {0, 0, 0};
  float orientation[6];
  memcpy(orientation, camera->focus, 3 * sizeof(float));
  memcpy(&(orientation[3]), camera->up, 3 * sizeof(float));
  alListenerfv(AL_POSITION, camera->pos);
  alListenerfv(AL_VELOCITY, velocity);
  alListenerfv(AL_ORIENTATION, orientation);
  if ((error = alGetError()) != AL_NO_ERROR) {
    return FALSE;
  }
  return TRUE;
}

static BOOL openal_stop(const sound_type* sound)
{
  ALenum error;
  UINT i;

  for (i = 0; i < sound->source_count; i++) {
    if (sound->source_handles[i] <= 0) {
      return FALSE;
    }

    alSourceStop(sound->source_handles[i]);
    if ((error = alGetError()) != AL_NO_ERROR) {
      return FALSE;
    }
  }
  return TRUE;
}

static void openal_uninit(void)
{
  if (openal_context) {
    alcDestroyContext(openal_context);
  }
  if (openal_device) {
    alcCloseDevice(openal_device);
  }
}

void sound_prepare_environment(void)
{
  sound_delete_func = &openal_delete;
  sound_generate_func = &openal_generate;
  sound_generate_ambient_func = &openal_generate_ambient;
  sound_init_func = &openal_init;
  sound_is_playing_func = &openal_is_playing;
  sound_set_listener_func = &openal_set_listener;
  sound_pause_func = &openal_pause;
  sound_play_at_func = &openal_play_at;
  sound_stop_func = &openal_stop;
  sound_uninit_func = &openal_uninit;
}

#elif defined(SOUND_CUSTOM)

#else
// no sound
void sound_prepare_environment(void) {}

#endif

